import tkinter as tk
from tkinter import ttk


class TipsEntry(ttk.Entry):
    __slots__ = "tips", "tips_color", "default_fg_color", "vcmd", "is_limited"

    def __init__(
        self,
        parent: tk.Misc,
        tips: str,
        tcolor: str = "grey",
        allowed_chars: str | None = None,
        **kwargs,
    ):
        """
        初始化带有占位符的 Entry 控件。
        parent: 父级窗口或控件
        tips: 提示文本
        pcolor: 提示文本的颜色
        allowed_chars: 限定允许输入的字符
        kwargs: 传给ttk.Entry的其他参数
        """
        super().__init__(parent, **kwargs)
        self.tips = tips
        self.tips_color = tcolor
        self.default_fg_color = self["foreground"]
        # 绑定事件
        self.bind("<FocusIn>", self._focus_in)
        self.bind("<FocusOut>", self._focus_out)
        # 检查是否需要限定输入字符
        self.is_limited: bool = False if allowed_chars is None else True
        if allowed_chars is not None:
            allowed_set = set(allowed_chars)  # 限定的输入的字符串集合
            # 注册验证函数
            self.vcmd = (
                self.register(lambda v: all(char in allowed_set for char in v)),
                "%P",
            )
            self.config(validate="key", validatecommand=self.vcmd)
        # 初始时显示提示文本
        self.show_tips()

    def get_input(self) -> str:
        """获取实际输入的数据,而不是当前输入框里的文本"""
        cur_input = self.get()
        # 当前输入框的输入文本为提示文本,则直接返回空字符串
        if cur_input == self.tips:
            return ""
        # 输入框里有实际输入
        return cur_input

    def init_value(self, value: str):
        """初始化Entry里的实际文本"""
        # 临时禁用输入限定验证
        self.config(validate="none")
        self.delete("0", "end")
        self.insert(0, value)
        self["foreground"] = self.default_fg_color
        # 重新启用输入限定验证
        if self.is_limited:  # 只有在有输入限定时才重新启用验证
            self.config(validate="key", validatecommand=self.vcmd)
        # 重新检测一下是否输入的是否为空字符串
        self._focus_out()

    def show_tips(self) -> None:
        """在Entry控件中插入提示文本"""
        # 临时禁用输入限定验证
        self.config(validate="none")
        self.insert(0, self.tips)
        self["foreground"] = self.tips_color
        # 重新启用输入限定验证
        if self.is_limited:  # 只有在有输入限定时才重新启用验证
            self.config(validate="key", validatecommand=self.vcmd)

    def _focus_in(self, event=None):
        """当Entry控件获得焦点时,删除提示文本"""
        if not self.get_input():
            self.delete("0", "end")
            self["foreground"] = self.default_fg_color

    def _focus_out(self, event=None):
        """当Entry控件失去焦点时,显示提示文本"""
        if not self.get():
            self.show_tips()


class SelectCardFrame(ttk.Frame):
    __slots__ = (
        "canvas",
        "scrollbar",
        "cards_frame",
        "cards_frame_idx",
        "default_entry",
        "cards_ls",
    )

    def __init__(self, parent, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)
        # 创建一个 Canvas 作为滚动区域
        self.canvas = tk.Canvas(self, background="#ffffff")
        self.canvas.pack(side="left", fill="y")  # 放置Canvas
        self.scrollbar = ttk.Scrollbar(
            self, orient="vertical", command=self.canvas.yview
        )
        self.scrollbar.pack(side="left", fill="y")  # 放置Scrollbar
        self.canvas.configure(yscrollcommand=self.scrollbar.set)  # 配置滚动条
        # 创建一个Frame作为Canvas的内部容器,用来放置标签卡Card
        self.cards_frame = ttk.Frame(self)
        # 将内部Frame放置在Canvas上并且记录其索引号用于更新Frame宽度
        self.cards_frame_idx = self.canvas.create_window(
            (0, 0), window=self.cards_frame, anchor="nw"
        )
        # 绑定Canvas的<Configure>事件,表示当前Canvas的配置发生改变是调用回调函数
        self.canvas.bind("<Configure>", self._update_canvas)
        # 创建控制标签卡的按键
        ttk.Button(
            self.cards_frame,
            text="添加标签卡",
            command=self.push_card,
        ).pack(side="top", fill="x")
        ttk.Button(
            self.cards_frame,
            text="删除标签卡",
            command=self.pop_card,
        ).pack(side="top", fill="x")
        self.default_entry = TipsEntry(
            self.cards_frame,
            tips="输入标签的默认值,默认为None",
            allowed_chars=r"0123456789.",
        )
        self.default_entry.pack(side="top", fill="x")
        # 创建空的用于保存标签卡的列表
        self.cards_ls: list[LabelCard] = []
        self.init_cards({"": ""})  # 添加初始标签卡

    def init_cards(self, value_dict: dict[str, str]):
        """初始化标签卡"""
        # 先删除所有现存的标签卡
        for _ in range(len(self.cards_ls)):
            self.pop_card()
        # 重新根据传入的字典来添加标签卡
        for label_value, data_value in value_dict.items():
            self.push_card(label_value, data_value)

    def init_default_value(self, value: str):
        """初始化设置默认标签的值"""
        self.default_entry.init_value(value)

    def push_card(self, label_init_value: str = "", data_init_value: str = ""):
        """压入标签卡,同时更新canvas的下拉栏"""
        self.cards_ls.append(LabelCard(self.cards_frame, len(self.cards_ls)))
        self.cards_ls[-1].init_label_value(label_init_value)
        self.cards_ls[-1].init_data_value(data_init_value)
        self.cards_ls[-1].pack(side="top", padx=2, pady=5, fill="both")
        self._update_canvas()

    def pop_card(self):
        """弹出并删除标签卡,同时更新canvas的下拉栏"""
        if self.cards_ls:
            card = self.cards_ls.pop()
            card.destroy()
            del card
            self._update_canvas()

    def _update_canvas(self, event=None):
        """更新canvas的可下拉高度和更新cards_frame的宽度"""
        box = self.canvas.bbox("all")
        box = (box[0], box[1], box[2], box[3] + 150)
        self.canvas.configure(scrollregion=box)
        if event is not None:
            self.canvas.itemconfig(self.cards_frame_idx, width=event.width)

    def get_default_input(self) -> str:
        """获取默认值的实际文本框的字符串"""
        return self.default_entry.get_input()

    def get_cards_dict(self) -> dict[str, str]:
        """获取标签卡当前的设置列表"""
        setting_dict: dict[str, str] = {}
        for card in self.cards_ls:
            label_str, data_str = card.get_input()
            setting_dict[label_str] = data_str
        return setting_dict

    def get_all_label(self) -> dict[str, int | float | None]:
        """整合并获取所有标签的数据"""
        res_dict: dict[str, int | float | None] = {}
        for card in self.cards_ls:
            # 获取默认值输入框的值,不输入则设为None
            default = self.default_entry.get_input().replace(" ", "")
            try:  # 转换成数值类型
                default = int(default) if default.isdigit() else float(default)
            except ValueError:  # 捕获不能转换为整数浮点类型的数据
                default = None
            res_dict.update(card.get_data(default))
        return res_dict


class LabelCard(ttk.Frame):
    __slots__ = "label_entry", "data_entry"

    def __init__(self, parent, indax_text, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)
        ttk.Label(  # 显示当前标签卡的序号
            self,
            text=indax_text,
            background="#ff5f5f",
            anchor="center",
        ).grid(row=0, column=0, sticky="nsew")
        # 创建标签名输入框
        self.label_entry = TipsEntry(self, tips="请输入标签名")
        self.label_entry.grid(row=0, column=1, sticky="nsew")
        # 创建标签数据输入框
        self.data_entry = TipsEntry(
            self,
            tips="请输入对应的数值型的标签,多个标签用英文逗号分开",
            allowed_chars=r"0123456789., ",
        )
        self.data_entry.grid(row=1, column=0, columnspan=2, sticky="nsew")
        # 配置列权重
        self.columnconfigure(0, weight=1)
        self.columnconfigure(1, weight=9)

    def init_label_value(self, value: str):
        """初始化直接设置标签文本框里的实际文本"""
        self.label_entry.init_value(value)

    def init_data_value(self, value: str):
        """初始化直接设置数据文本框里的实际文本"""
        self.data_entry.init_value(value)

    def get_input(self) -> tuple[str, str]:
        """获取文本框里的实际文本"""
        return self.label_entry.get_input(), self.data_entry.get_input()

    def get_data(
        self, default: int | float | None = None
    ) -> dict[str, int | float | None]:
        """获取并解析标签卡的数据,返回数据字典"""
        label: str = self.label_entry.get_input()
        data: list[str] = self.data_entry.get_input().replace(" ", "").split(",")
        # 将获取到的数据变成字典
        data_dict: dict[str, int | float | None] = {}
        # 如果只输入了一个数则不用加索引号直接返回一个字典
        for i, elm in enumerate(data):
            # 为后面的数据起不同的字典名
            cur_label = f"{label}_{i}" if len(data) != 1 else label
            try:  # 将数据加入字典
                # 如果元素是纯数字没有小数点之类的,则转成int,否则转float
                data_dict[cur_label] = int(elm) if elm.isdigit() else float(elm)
            except ValueError:  # 捕获不能转换为整数或浮点类型的数据
                # 将不能转换的数据设置为默认值
                data_dict[cur_label] = default
        return data_dict
